package com.box.app.arithmetic

internal class Rpn(private val factors: List<Factor>) {
    @Throws(ArithmeticError::class)
    fun call(variables: Map<String, Number>? = null): Number {
        val rpnQueue = toRpn().toMutableList()
        val outputStack = mutableListOf<Any>()
        while (rpnQueue.isNotEmpty()) {
            val (index, current) = rpnQueue.removeFirst()
            when (current) {
                is Operator1 -> {
                    val factor = valueOf(outputStack.removeLastOrNull()).let {
                        it ?: throw ArithmeticError.operator(factors, index, current)
                    }
                    try {
                        outputStack += current.call(factor)
                    } catch (e: Throwable) {
                        throw ArithmeticError(e, factors, index, current)
                    }
                }

                is Operator2 -> {
                    val factor2 = valueOf(outputStack.removeLastOrNull())
                    val factor1 = valueOf(outputStack.removeLastOrNull())
                    if (factor1 == null || factor2 == null) {
                        throw ArithmeticError.operator(factors, index, current)
                    }
                    try {
                        outputStack += current.call(factor1, factor2)
                    } catch (e: Throwable) {
                        throw ArithmeticError(e, factors, index, current)
                    }
                }

                is Math -> {
                    val factor = valuesOf(outputStack.removeLastOrNull()).let {
                        it ?: throw ArithmeticError.function(factors, index, current)
                    }
                    try {
                        outputStack += current.call(*factor.toTypedArray())
                    } catch (e: Throwable) {
                        throw ArithmeticError(e, factors, index, current)
                    }
                }

                is Variables -> {
                    outputStack.add(current.parse(variables) { before, argument, error ->
                        val arguments = before + argument
                        val length = Variables(arguments).toDescribe().length - argument.toDescribe().length
                        val newIndex = index + length
                        when (argument) {
                            is Variable1 -> throw ArithmeticError(error, factors, newIndex, argument)
                            is Expression -> throw ArithmeticError(error, factors, newIndex)
                        }
                    })
                }
            }
        }
        return checkNotNull(valueOf(outputStack.first()))
    }

    @Throws(ArithmeticError::class)
    private fun toRpn(): List<Indexed<Operable>> {
        val inputStack = factors.toMutableList()
        val operatorStack = mutableListOf<Indexed<Factor>>()
        val outputStack = mutableListOf<Indexed<Operable>>()
        var index = 0
        while (inputStack.isNotEmpty()) {
            val current = inputStack.removeFirst()
            when (current) {
                is Variables -> outputStack.add(Indexed(index, current as Operable))
                Curve.left -> operatorStack.add(Indexed(index, current))
                Curve.right -> {
                    if (operatorStack.isEmpty()) {
                        throw ArithmeticError.curveRight(factors, index, current)
                    }
                    var last = operatorStack.removeLast()
                    while (last.value !== Curve.left) {
                        outputStack.add(last.to())
                        last = if (operatorStack.isNotEmpty()) {
                            operatorStack.removeLast()
                        } else {
                            break
                        }
                    }
                    if (last.value !== Curve.left) {
                        throw ArithmeticError.curveLeft(factors, last.index, last.value)
                    }
                }

                else -> {
                    while (operatorStack.isNotEmpty() && current <= operatorStack.last().value) {
                        val last = operatorStack.removeLast()
                        if (current.priority == last.value.priority && current is Operator1) {
                            operatorStack.add(last)
                            break
                        } else {
                            outputStack.add(last.to())
                        }
                    }
                    operatorStack.add(Indexed(index, current))
                }
            }
            index += current.toDescribe().length
        }
        while (operatorStack.isNotEmpty()) {
            val last = operatorStack.removeLast()
            if (last.value is Curve) {
                throw ArithmeticError.curve(factors, last.index, last.value)
            }
            outputStack.add(last.to())
        }
        return outputStack
    }
}

private fun valueOf(factor: Any?): Number? {
    return when (factor) {
        is Number -> factor
        is List<*> -> valueOf(factor.lastOrNull())
        else -> null
    }
}

private fun valuesOf(factor: Any?): List<Number>? {
    return when (factor) {
        is Number -> listOf(factor)
        is List<*> -> factor.map { valueOf(it)!! }
        else -> null
    }
}